﻿using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace Remote_server
{
    /// <summary>
    /// This class is used to communicate with
    /// any connecting clients, it is also possible to
    /// send commands
    /// </summary>
    public class RspClient : IDisposable
    {
        /// <summary>
        /// A list with active commands, any commands
        /// whose names are not in this list will not
        /// surface with the CommandReceived event
        /// </summary>
        public List<string> Commands { get; set; }
        /// <summary>
        /// This event gets raised when a command from the remote
        /// host is received and the event dispatcher is started
        /// through the StartEventDispatcher method
        /// </summary>
        public event CommandReceivedEventHandler CommandReceived;
        /// <summary>
        /// This event gets raised when the client is closed,
        /// either by the remote host or by calling Close()
        /// </summary>
        public event Action<RspClient> Closed;

        //Underlying connection client
        private TcpClient client;
        //Reading stream
        private StreamReader sr;
        //Writing stream
        private StreamWriter sw;
        //The remote address is cached, so that it may
        //be retreived even when he remote host has
        //disconnected
        private string remoteAddr = "";

        /// <summary>
        /// Initialize this RspClient with an already
        /// connected TcpClient
        /// </summary>
        /// <param name="tcpClient">The TcpClient</param>
        public RspClient(TcpClient tcpClient)
        {
            client = tcpClient;
            sr = new StreamReader(tcpClient.GetStream());
            sw = new StreamWriter(tcpClient.GetStream());
            remoteAddr = client.Client.RemoteEndPoint.ToString();
        }

        /// <summary>
        /// Connect to a remote host directly
        /// </summary>
        /// <param name="remoteHost">The address of the remote host,
        /// this may be an IP address or a domain name</param>
        /// <param name="port">The port to connect to</param>
        public RspClient(string remoteHost, ushort port)
        {
            client = new TcpClient();
            client.Connect(remoteHost, (int)port);
            sr = new StreamReader(client.GetStream());
            sw = new StreamWriter(client.GetStream());
            remoteAddr = client.Client.RemoteEndPoint.ToString();
        }

        /// <summary>
        /// Starts a listening thread that dispatches
        /// any received and allowed commands asynchronously using the
        /// CommandReceived event. Starting the event dispatcher several
        /// times causes undefined behaviour
        /// </summary>
        public void StartEventDispatcher()
        {
            Thread t = new Thread(delegate()
            {
                while (true)
                    try
                    {
                        Command cmd = ReadCommand();
                        if (Commands.Contains(cmd.Name))
                            RaiseCommandReceivedEvent(cmd);
                        else if (cmd.Name == "LOGOUT")
                        {
                            Close();
                            return;
                        }
                        else
                            SendErrorMessage();
                    }
                    catch { break; }
                Close();
            });
            t.IsBackground = true;
            t.Start();
        }

        /// <summary>
        /// Send a command to the remote host
        /// </summary>
        /// <param name="command">The name of the command</param>
        /// <param name="parameters">Optional supplementary parameters</param>
        public void SendCommand(string command, string parameters = "")
        {
            sw.WriteLine(String.Format(
                "{0}{1}", command, (parameters == "" ? "" : " " + parameters)));
        }

        /// <summary>
        /// Read a command from the remote host,
        /// synchronously
        /// </summary>
        /// <returns>The Command received</returns>
        public Command ReadCommand()
        {
            string ln = sr.ReadLine();
            return new Command(ln);
        }

        /// <summary>
        /// Dispose this RspClient, this closes
        /// the underlying connection
        /// </summary>
        public void Dispose()
        {
            Close();
        }

        /// <summary>
        /// Close the connection
        /// </summary>
        public void Close()
        {
            try
            {
                sr.Close();
                sw.Close();
                client.Close();
                RaiseCloseEvent();
            }
            catch { }
        }

        /// <summary>
        /// Get the address of the remote host,
        /// specified as [IP:PORT]
        /// </summary>
        public string RemoteAddress
        {
            get { return remoteAddr; }
        }

        /// <summary>
        /// Send an error message to the remote host,
        /// if not specifying a message the default error
        /// message will be sent (only an exclamation mark)
        /// </summary>
        /// <param name="msg">Optional message description</param>
        public void SendErrorMessage(string msg = "")
        {
            sw.WriteLine("!" + (msg.StartsWith(" ") ? msg : " " + msg));
            sw.Flush();
        }

        /// <summary>
        /// Raises a Command, if the Command is not
        /// considered Handled after the bubbling, an error
        /// message will be sent
        /// </summary>
        /// <param name="cmd">The Command to raise</param>
        private void RaiseCommandReceivedEvent(Command cmd)
        {
            if (CommandReceived != null)
            {
                CommandReceivedEventArgs evt = new CommandReceivedEventArgs(cmd);
                CommandReceived(this, evt);
                if (!evt.Handled)
                    SendErrorMessage(evt.Error);
            }
        }

        /// <summary>
        /// Raise the Closed event
        /// </summary>
        private void RaiseCloseEvent()
        {
            if (Closed != null)
                Closed(this);
        }
    }
}
